package edu.fiu.cis.selfhealingdisk;

import java.util.HashMap;
import java.util.Map.Entry;

import edu.fiu.cis.selfhealingdisk.Configuration.ParityFormatEnum;


/**
 * @author vanvorst
 *
 */
public class DiskLayout {
	private HashMap<Long, ParityGroup> parityGroups = new HashMap<Long, ParityGroup>();
	private HashMap<Long, BlockUnit> blockUnits = new HashMap<Long, BlockUnit>();
	private HashMap<BlockUnit, ParityGroup> parityMap = new HashMap<BlockUnit, ParityGroup>();
	private final BlockMapper mapper;
	
	
	/**
	 * @param config
	 * @param totalBlocks
	 */
	public DiskLayout(Configuration config, long minNumberOfBlocks) {
		long totalBlocks=0;
		switch(config.getParityLayout()) {
		case MOD:
			mapper=new ModBlockMapper(config,minNumberOfBlocks);
			break;
		default:
			throw new RuntimeException("Unknown parity layout "+config.getParityLayout());
		}
		if(config.getParityFormat()!=ParityFormatEnum.NOPARITY) {
			totalBlocks=configureModLayout();
			if(totalBlocks<minNumberOfBlocks) {
				throw new RuntimeException("Problem laying out the disk. The minimum number of blocks were not allocated!");
			}
		}
	}
	
	/**
	 * Get a copy of the current parity map. 
	 * The map is the parityUnit id to the parityGroup id.
	 * 
	 * @return
	 */
	public HashMap<Long,Long> getCurrentParityMap() {
		HashMap<Long,Long> rv = new HashMap<Long, Long>();
		for(Entry<BlockUnit, ParityGroup> e :this.parityMap.entrySet()) {
			rv.put(e.getKey().getId(), e.getValue().getId());
		}
		return rv;
	}
	
	/**
	 * Get a copy of the current BlockMap
	 * 
	 * @return
	 */
	public HashMap<Long, BlockUnit> getCompleteBlockUnitLayout() {
		HashMap<Long,BlockUnit> rv = new HashMap<Long, BlockUnit>();
		for(Entry<Long, BlockUnit> e :this.blockUnits.entrySet()) {
			rv.put(e.getKey(), e.getValue());
		}
		return rv;	
	}
	
	
	/**
	 * Mark the data unit at unitId as updated at timestamp.
	 * associated parity group as updated.
	 * 
	 * @param timestamp
	 * @param unitId
	 * @throws IllegalArgumentException if the unit at unitId is a parity unit
	 */
	public void updateDataUnit(final long timestamp, final long unitId) throws IllegalArgumentException {
		BlockUnit unit = this.blockUnits.get(unitId);
		if(unit.getType()==BlockUnit.BlockTypeEnum.PARITY_BLOCK) {
			throw new IllegalArgumentException("Passed in a parity unit!");
		}
		unit.setLastUpdate(timestamp);
	}
		
	/**
	 * Mark the parity unit at unitId as updated at timestamp and
	 * Mark all of the data units associated with the parity group parityGroupId
	 * as updated at the timestamp.
	 * 
	 * @param timestamp
	 * @param unitId
	 * @throws IllegalArgumentException if the unit at unitId is not a parity unit
	 */
	public void updateParityUnit(final long timestamp, final long unitId, final long parityGroupId) throws IllegalArgumentException {
		final BlockUnit bu = this.blockUnits.get(unitId);
		if(bu.getType()==BlockUnit.BlockTypeEnum.DATA_BLOCK) {
			throw new IllegalArgumentException("Passed in a data unit!");
		}
		final ParityGroup pg = parityGroups.get(parityGroupId);
		bu.setLastUpdate(timestamp);
		pg.synchronizeParity(timestamp);
		parityMap.put(bu, pg);
	}
	
	/**
	 * @param physicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcLogicalBlock(long)
	 */
	public long calcLogicalBlock(long physicalBlock) {
		return mapper.calcLogicalBlock(physicalBlock);
	}
		
	/**
	 * @param logicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcPhysicalBlock(long)
	 */
	public long calcPhysicalBlock(long logicalBlock) {
		return mapper.calcPhysicalBlock(logicalBlock);
	}

	/**
	 * @param logicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcParityGroupFromLogicalBlock(long)
	 */
	public long calcParityGroupFromLogicalBlock(long logicalBlock) {
		return mapper.calcParityGroupFromLogicalBlock(logicalBlock);
	}

	/**
	 * @param physicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcParityGroupFromPhysicalBlock(long)
	 */
	public long calcParityGroupFromPhysicalBlock(long physicalBlock) {
		return mapper.calcParityGroupFromPhysicalBlock(physicalBlock);
	}
	
	/**
	 * @param logicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcParityGroupFromLogicalBlock(long)
	 */
	public long calcParityUnitFromLogicalBlock(long logicalBlock) {
		return parityGroups.get(mapper.calcParityGroupFromLogicalBlock(logicalBlock)).getId();
	}

	/**
	 * @param physicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcParityGroupFromPhysicalBlock(long)
	 */
	public long calcParityUnitFromPhysicalBlock(long physicalBlock) {
		return parityGroups.get(mapper.calcParityGroupFromPhysicalBlock(physicalBlock)).getId();
	}

	/**
	 * @param logicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcUnitFromLogicalBlock(long)
	 */
	public long calcUnitFromLogicalBlock(long logicalBlock) {
		return mapper.calcUnitFromLogicalBlock(logicalBlock);
	}

	/**
	 * @param physicalBlock
	 * @return
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcUnitFromPhysicalBlock(long)
	 */
	public long calcUnitFromPhysicalBlock(long physicalBlock) {
		return mapper.calcUnitFromPhysicalBlock(physicalBlock);
	}
	
	/**
	 * 
	 * @param physicalBlock
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calcParityBlocks(long)
	 */
	public long[] calcParityBlocks(long physicalBlock) {
		return mapper.calcParityBlocks(physicalBlock);
	}
	
	/**
	 * 
	 * @param physicalBlock
	 * @see edu.fiu.cis.selfhealingdisk.BlockMapper#calRelPositionInPhysicalUnit(long)
	 */
	public int calcRelPositionInPhysicalUnit(long physicalBlock) {
		return mapper.calcRelPositionInPhysicalUnit(physicalBlock);
	}
	
	public long calcParityBlock(long logicalBlock) {
		long pba = mapper.calcPhysicalBlock(logicalBlock);
		//relative position in the data_unit + first_block in the correspondent parity unit
		return this.calcRelPositionInPhysicalUnit(pba) + calcParityBlocks(pba)[0];
		
	}
	
	
	
	/**
	 * @param id
	 * @return
	 */
	public BlockUnit getUnit(final long id) {
		return this.blockUnits.get(id);
	}
	
	/**
	 * @param id
	 * @return
	 */
	public ParityGroup getGroup(final long id) {
		return this.parityGroups.get(id);
	}

	/**
	 * @return
	 */
	protected long configureModLayout() {
		long totalUnits=0;
		long totalBlocks=0;
		long groupIndex=0;
		for(int i=0;i<mapper.requiredPartitions();i++) {
			for(int j=0;j<mapper.getGroupsPerPartiton();j++) {
				this.parityGroups.put(groupIndex,new ParityGroup(groupIndex));
				groupIndex++;
			}
			for(int j=0;j<mapper.getUnitsPerPartition();j++) {
				ParityGroup pg=parityGroups.get(j%mapper.getModIndex());
				BlockUnit bu=null;
				if(pg.size()==mapper.getParityIndex()) {
					bu=new BlockUnit(pg,
										totalUnits,
										totalBlocks,
										mapper.getBlocksPerParityUnit(),
										BlockUnit.BlockTypeEnum.PARITY_BLOCK);
					parityMap.put(bu, pg);
					totalBlocks+=mapper.getBlocksPerParityUnit();
				}
				else {
					bu=new BlockUnit(pg,
										totalUnits,
										totalBlocks,
										mapper.getBlocksPerDataUnit(),
										BlockUnit.BlockTypeEnum.DATA_BLOCK);
					totalBlocks+=mapper.getBlocksPerDataUnit();
				}
				pg.add(bu);
				this.blockUnits.put(bu.getId(), bu);
				totalUnits++;
			}
		}
		
		if(Main.DEBUG) {
			System.out.println("\t[DEBUG]total Data/Parity Units:"+this.blockUnits.size());
			System.out.println("\t[DEBUG]total Parity Groups:"+this.parityGroups.size());
			System.out.println("\t[DEBUG]total Blocks:"+totalBlocks);
			System.out.flush(); 
		}
		return totalBlocks;
	}
	
}
